﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using Charisma.Linq;
using Charisma.MongoDb.Linq;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
using MongoDB.Bson.Serialization;

namespace Charisma.MongoDB
{
    internal sealed class Repository<T> : IRepository<T> where T : class
    {
        private readonly MongoCollection<T> collection;
        private readonly BsonMemberMap keyAccessor;
        private readonly IDbProvider provider;
        private readonly Queryable<T> queryRoot;

        public Repository(IDbProvider provider, Database database, RepositoryConfiguration configuration)
        {
            this.provider = provider;
            queryRoot = new Queryable<T>(new QueryProvider(database.Mongo, configuration.Name));
            keyAccessor = BsonClassMap.LookupClassMap(typeof(T)).IdMemberMap;
            collection = database.Mongo.GetCollection<T>(configuration.Name);
        }

        #region IRepository<T> Members

        public Type ElementType
        {
            get { return typeof (T); }
        }

        public IQueryable<T> All
        {
            get { return queryRoot; }
//            get { return collection.AsQueryable(); }
        }

        public T Find(object key)
        {
            return collection.FindOneByIdAs<T>(BsonValue.Create(key));
        }

        public void Add(T item)
        {
            SafeModeResult result;
            try {
                result = collection.Insert(item);
            }
            catch (Exception e) {
                throw provider.TranslateException("Error adding object to repository, see inner exception for details.", e);
            }
            HandleResult(result);
        }

        public void Add(IEnumerable<T> items)
        {
            IEnumerable<SafeModeResult> results;
            try {
                results = collection.InsertBatch(items);
            }
            catch (Exception e) {
                throw provider.TranslateException("Error adding objects to repository, see inner exception for details.", e);
            }
            HandleResult(results);
        }

        public void Remove(T item)
        {
            SafeModeResult result;
            try {
                var key = keyAccessor.Getter.Invoke(item);
                var query = Query.EQ(keyAccessor.ElementName, BsonValue.Create(key));
                result = collection.Remove(query);
            }
            catch (Exception e) {
                throw provider.TranslateException("Error removing object from repository, see inner exception for details.", e);
            }
            HandleResult(result);
        }

        public void Update(T item)
        {
            SafeModeResult result;
            try {
                var key = keyAccessor.Getter.Invoke(item);
                var query = Query.EQ(keyAccessor.ElementName, BsonValue.Create(key));
                var update = global::MongoDB.Driver.Builders.Update.Replace(item);
                result = collection.Update(query, update);
                if (result != null && !result.Ok)
                    throw new CharismaException(result.ErrorMessage);
            }
            catch (Exception e) {
                throw provider.TranslateException("Error updating object, see inner exception for details.", e);
            }
            HandleResult(result);
        }

        public void Update(T item, params Expression<Func<T, object>>[] fields)
        {
            SafeModeResult result = null;
            try {
                var key = keyAccessor.Getter.Invoke(item);
                var query = Query.EQ(keyAccessor.ElementName, BsonValue.Create(key));
                var builder = new UpdateBuilder();
                var map = BsonClassMap.LookupClassMap(typeof(T));
                foreach (var field in fields) {
                    var me = field.Body as MemberExpression;
                    if (me == null)
                        throw new NotSupportedException(
                            string.Format("Expression other then {0} are not supported yet", typeof(MemberExpression).Name));
                    var memberMap = map.MemberMaps.FirstOrDefault(mm => mm.MemberInfo == me.Member);
                    if (memberMap == null)
                        continue;
                    if (memberMap == map.IdMemberMap)
                        throw new InvalidOperationException("Key property may not be updated.");
                    builder = builder.Set(memberMap.ElementName, BsonValue.Create(memberMap.Getter.Invoke(item)));
                }
                result = collection.Update(query, builder);
            }
            catch (Exception e)
            {
                throw provider.TranslateException("Error updating object, see inner exception for details.", e);
            }
            HandleResult(result);
        }

        #endregion


        private static void HandleResult(SafeModeResult result)
        {
            if (result != null && !result.Ok)
                throw new CharismaException(result.ErrorMessage);
        }

        private static void HandleResult(IEnumerable<SafeModeResult> results)
        {
            var failures = (results ?? Enumerable.Empty<SafeModeResult>())
                .Where(r => !r.Ok)
                .ToList();
            if (failures.Count > 0)
                throw new CharismaException(string.Join(Environment.NewLine, failures.Select(r => r.ErrorMessage)));
        }

    }
}